{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[toc]\n",
    "\n",
    "## 一、运行环境\n",
    "\n",
    "- python3.7 - **32 位！32 位！32 位！**\n",
    "\n",
    "- KEPServerEX.6.4  \n",
    "    下载地址：https://pan.baidu.com/s/1R4bC-NKLEl4s4FqRfhAIwQ \n",
    "提取码：07yy\n",
    "\n",
    "- OPCDAAuto.dll  \n",
    "    - 首先进入`C:\\Windows\\System32` 目录下，检查系统是否经有同名文件（忽略大小写），如果没有或者 在使用中发生注册失败的错误，请先下载一份，保存路径随意  \n",
    "    - 下载地址：https://pan.baidu.com/s/1xjzRixvFQ5RNzrs6_IpB2w \n",
    "    提取码：kpzo\n",
    "\n",
    "## 二、开始使用\n",
    "\n",
    "### 1. 创建KEPServerEX.6.4测试工程\n",
    "\n",
    "关于软件是使用不做介绍，你也可以使用软件预置好的测试工程，比如点位： `通道 1.设备 1.TAG1`\n",
    "\n",
    "### 2. 注册dll\n",
    "\n",
    "\n",
    "进入存放 OPCDAAuto.dll 文件的路径，在cmd下执行以下命令\n",
    "\n",
    "```\n",
    "regsvr32 OPCDAAuto.dll \n",
    "```\n",
    "提示注册成功，或者已经注册即可进行下一步编程。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Coding\n",
    "\n",
    "1. 导入必要包"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import win32com.client\n",
    "from win32com.client import DispatchWithEvents\n",
    "from win32com.client import gencache"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果你的程序要通过pyistaller打包成可执行文件，需要额外导入以下包，防止编译完后由于module 缺失导致运行失败"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import win32timezone\n",
    "from win32com.client import Dispatch\n",
    "from win32com.client import DispatchWithEvents"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. 加载 dll 对象"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "OPC_DA_DLL = gencache.EnsureModule('{28E68F91-8D75-11D1-8DC3-3C302A000000}', 0, 1, 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果此处提示错误：\n",
    "`com_error: (-2147221164, '没有注册类', None, None)`\n",
    "，请先按照上面步骤 注册dll 到系统中\n",
    "\n",
    "\n",
    "3. 获取 opc server 对象"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "opcServer = OPC_DA_DLL.OPCServer()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果此处提示错误信息为：\n",
    "\n",
    "`com_error: (-2147221164, '没有注册类', None, None)`\n",
    "那就先检查你的python版本是不是 32位的\n",
    "\n",
    "\n",
    "4. 获取本机可用的 opc server地址"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Kepware.KEPServerEX.V6\n"
     ]
    }
   ],
   "source": [
    "node = '127.0.0.1'\n",
    "\n",
    "for svr in opcServer.GetOPCServers(node):\n",
    "    print(svr)\n",
    "\n",
    "# 本机只装了一个模拟器：Kepware.KEPServerEX.V6"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "5. 连接到服务器(本机\\远程)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "progID = 'Kepware.KEPServerEX.V6'\n",
    "node = '127.0.0.1'\n",
    "\n",
    "opcServer.Connect(progID, node)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "6. 拿到实例组"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "groups=opcServer.OPCGroups\n",
    "groups.DefaultGroupIsActive = True\n",
    "groups.DefaultGroupDeadband = 0\n",
    "groups.DefaultGroupUpdateRate = 200"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "7. 添加组，并设置属性"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<win32com.gen_py.None.OPCGroup>"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 组名\n",
    "group_name = '通道 1.设备 1'\n",
    "# 同一组名不要重复添加，group 对象可以先保存到list或者dict里\n",
    "group = groups.Add(group_name)\n",
    "group.IsActive = True\n",
    "group.IsSubscribed = True\n",
    "group.UpdateRate = 100\n",
    "\n",
    "group"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "8. 获取点位组对象"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = group.OPCItems"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "8. 添加需要操作的点位"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "tag_map = {}\n",
    "\n",
    "tag = \"通道 1.设备 1.TAG1\"\n",
    "# item 对象需要保存到list或者dict里，后续数据读写都要通过该对象\n",
    "# 注意第二个参数从1开始, 每添加一个点位就+ 1\n",
    "item = items.AddItem(tag, len(tag_map) + 1)\n",
    "# 点位不存在回返回None\n",
    "if item:\n",
    "    item.IsActive = True\n",
    "    tag_map[tag] = item"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "9. 读一个点位操作\n",
    "\n",
    "返回一个元组，（值，数据质量，时间），比如"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(200,\n",
       " 192,\n",
       " pywintypes.datetime(2020, 9, 6, 6, 18, tzinfo=TimeZoneInfo('GMT Standard Time', True)))"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "data = item.Read(win32com.client.constants.OPCDevice, 0, 0, 0)\n",
    "data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "10. 写入一个点位\n",
    "\n",
    "无返回值"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "item.Write(200)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "11. 断开连接\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "opcServer.Disconnect()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 三、自用代码参考"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "import re\n",
    "import time\n",
    "from typing import List\n",
    "\n",
    "import win32com.client\n",
    "from win32com.client import gencache\n",
    "\n",
    "# import win32timezone\n",
    "# from win32com.client import Dispatch\n",
    "# from win32com.client import DispatchWithEvents\n",
    "\n",
    "logging.basicConfig(level='DEBUG')\n",
    "logger = logging.getLogger('dll_dispatch')\n",
    "\n",
    "OPC_DA_DLL = gencache.EnsureModule('{28E68F91-8D75-11D1-8DC3-3C302A000000}', 0, 1, 0)\n",
    "opcServer = OPC_DA_DLL.OPCServer()\n",
    "group_name_map = {}  # 存放已添加的组名\n",
    "item_name_map = {}\n",
    "Groups = None\n",
    "\n",
    "\n",
    "def connect_opc(progID: str, node: str = '127.0.0.1'):\n",
    "    opcServer.Connect(progID, node)\n",
    "    logger.info('已连接到opc - [{}:{}]'.format(node, progID))\n",
    "    return opcServer\n",
    "\n",
    "\n",
    "def disconnect_opc(opcServer):\n",
    "    logger.info('opc断开连接')\n",
    "    opcServer.Disconnect()\n",
    "\n",
    "\n",
    "def get_servers(opcServer) -> List[str]:\n",
    "    \"\"\"查询可用opc服务\"\"\"\n",
    "    return opcServer.GetOPCServers()\n",
    "\n",
    "\n",
    "class GroupProperty:\n",
    "    # DeadBand\n",
    "    IsActive: bool = True\n",
    "    IsSubscribed: bool = True\n",
    "    UpdateRate: int = 1000\n",
    "    DefaultGroupIsActive: bool = True\n",
    "    DefaultGroupDeadband: int = 0\n",
    "\n",
    "\n",
    "# DefaultGroupProperty = GroupProperty()\n",
    "\n",
    "\n",
    "def get_groups(opcServer, groupProperty: GroupProperty):\n",
    "    global Groups\n",
    "    if not Groups:\n",
    "        Groups = opcServer.OPCGroups\n",
    "        Groups.DefaultGroupIsActive = groupProperty.IsActive\n",
    "        Groups.DefaultGroupDeadband = groupProperty.DefaultGroupDeadband\n",
    "        Groups.DefaultGroupUpdateRate = groupProperty.UpdateRate\n",
    "    return Groups\n",
    "\n",
    "\n",
    "def get_group(opcServer, opcGroupName: str, groupProperty: GroupProperty):\n",
    "    opcGroups = get_groups(opcServer, GroupProperty())\n",
    "    if opcGroupName not in group_name_map:\n",
    "        opcGroup = opcGroups.Add(opcGroupName)\n",
    "        opcGroup.IsActive = groupProperty.IsActive\n",
    "        opcGroup.UpdateRate = groupProperty.UpdateRate\n",
    "        opcGroup.IsSubscribed = groupProperty.IsSubscribed\n",
    "\n",
    "        group_name_map[opcGroupName] = opcGroup\n",
    "    else:\n",
    "        logger.warning('重复添加 GroupName - {}'.format(opcGroupName))\n",
    "\n",
    "    return group_name_map[opcGroupName]\n",
    "\n",
    "\n",
    "def get_items(opcServer, opcGroupName: str, groupProperty: GroupProperty):\n",
    "    group = get_group(opcServer, opcGroupName, groupProperty)\n",
    "    return group.OPCItems\n",
    "\n",
    "\n",
    "def add_item(opcServer, itemName: str):\n",
    "    \"\"\"添加一个点位\"\"\"\n",
    "    if itemName not in item_name_map:\n",
    "        try:\n",
    "            groupName = re.split(r'[.$][^.$]*?$', itemName, 1)[0]\n",
    "        except IndexError:\n",
    "            logger.error('点位名-{}-不符合规则'.format(itemName))\n",
    "            return\n",
    "        items = get_items(opcServer, groupName, GroupProperty())\n",
    "        try:\n",
    "            item = items.AddItem(itemName, len(item_name_map) + 1)\n",
    "            # 添加失败时 item 为 None\n",
    "            if not items:\n",
    "                raise RuntimeError('add item[{}] return None'.format(itemName))\n",
    "        except Exception as e:\n",
    "            logger.error('添加点位-{}-失败! [点位可能不存在]'.format(itemName), exc_info=True)\n",
    "            item_name_map[itemName] = None\n",
    "        else:\n",
    "            item.IsActive = True\n",
    "            time.sleep(0.1)\n",
    "            item_name_map[itemName] = item\n",
    "    else:\n",
    "        logger.debug('重复添加 itemName - {}'.format(itemName))\n",
    "\n",
    "\n",
    "def add_items(opcServer, item_list: List[str]):\n",
    "    \"\"\"批量添加点位\"\"\"\n",
    "    for item in item_list:\n",
    "        add_item(opcServer, item)\n",
    "\n",
    "\n",
    "def _sync_read(item):\n",
    "    data = item.Read(win32com.client.constants.OPCDevice, 0, 0, 0)\n",
    "    if not data:\n",
    "        raise ValueError('sync_read return None.')\n",
    "    return data\n",
    "\n",
    "\n",
    "def sync_read_item(opcServer, itemName: str):\n",
    "    \"\"\"\n",
    "    同步读取一个 opc 点位\n",
    "    :param opcServer:\n",
    "    :param itemName:\n",
    "    :return:  (数据, 数据质量, 时间)\n",
    "    \"\"\"\n",
    "    if itemName not in item_name_map:\n",
    "        add_item(opcServer, itemName)\n",
    "    logger.debug('读取 - {}'.format(itemName))\n",
    "    item = item_name_map[itemName]\n",
    "    try:\n",
    "        if not item:\n",
    "            raise ValueError('点位不存')\n",
    "        return _sync_read(item)\n",
    "    except Exception as e:\n",
    "        logger.error('采集失败: {} - {}'.format(itemName, e), exc_info=True)\n",
    "        return None, 0, 0\n",
    "\n",
    "\n",
    "def sync_read_items(opcServer, itemNameList: List[str]):\n",
    "    result = []\n",
    "    for itemName in itemNameList:\n",
    "        if itemName not in item_name_map:\n",
    "            add_item(opcServer, itemName)\n",
    "\n",
    "        result.append(sync_read_item(opcServer, itemName))\n",
    "    return result\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    opcServer = connect_opc('Kepware.KEPServerEX.V6', '127.0.0.1')\n",
    "    print(get_servers(opcServer))\n",
    "    tag_list = ['T1.T1.Tag1', 'T1.T1.Tag2', 'T1.T1.Tag3']\n",
    "    try:\n",
    "        while True:\n",
    "            data = sync_read_items(opcServer, tag_list)\n",
    "            for i in data:\n",
    "                print(i[:2])\n",
    "            time.sleep(2)\n",
    "    except Exception:\n",
    "        logger.error('异常退出', exc_info=True)\n",
    "    finally:\n",
    "        opcServer.Disconnect()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**参考**\n",
    "\n",
    "- https://github.com/meilidao12/OPCForZuTaiWang/blob/ff0d03f419/CommunicationServers/OPC/OPCClientHelper.cs"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py38_x86",
   "language": "python",
   "name": "py38_x86"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.4rc1"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
